using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;

namespace Decav.Gatsb.Locales
{
    /// <summary>
    /// The locale for the greater New York City area.
    /// </summary>
    public class NewYorkCityLocale : Locale
    {
        public NewYorkCityLocale()
            : base("New York City",
                new Guid("{C2A7E262-1075-4b2f-8CEB-7D1571C1CD6F}"))
        {
            InitializeNeighborhoods();
        }

        private void InitializeNeighborhoods()
        {
            AddNeighborhood("Crotona - Tremont", "Brx", "Bronx", "10453", "10457", "10460");
            AddNeighborhood("Fordham - Bronx Park", "Brx", "Bronx", "10458", "10467", "10468");
            AddNeighborhood("High Bridge - Morrisania", "Brx", "Bronx", "10451", "10452", "10456");
            AddNeighborhood("Hunts Point - Mott Haven", "Brx", "Bronx", "10454", "10455", "10459", "10474");
            AddNeighborhood("Kingsbridge - Riverdale", "Brx", "Bronx", "10463", "10471");
            AddNeighborhood("Northeast Bronx", "Brx", "Bronx", "10466", "10469", "10470", "10475");
            AddNeighborhood("Pelham - Throgs Neck", "Brx", "Bronx", "10461", "10462,10464", "10465", "10472", "10473");
            AddNeighborhood("Bedford Stuyvesant - Crown Heights", "Bklyn", "Brooklyn", "11212", "11213", "11216", "11233", "11238");
            AddNeighborhood("Bensonhurst - Bay Ridge", "Bklyn", "Brooklyn", "11209", "11214", "11228");
            AddNeighborhood("Borough Park", "Bklyn", "Brooklyn", "11204", "11218", "11219", "11230");
            AddNeighborhood("Canarsie - Flatlands", "Bklyn", "Brooklyn", "11234", "11236", "11239");
            AddNeighborhood("Coney Island - Sheepshead Bay", "Bklyn", "Brooklyn", "11223", "11224", "11229", "11235");
            AddNeighborhood("Downtown - Heights - Slope", "Bklyn", "Brooklyn", "11201", "11205", "11215", "11217", "11231");
            AddNeighborhood("East Flatbush - Flatbush", "Bklyn", "Brooklyn", "11203", "11210", "11225", "11226");
            AddNeighborhood("East New York", "Bklyn", "Brooklyn", "11207", "11208");
            AddNeighborhood("Greenpoint", "Bklyn", "Brooklyn", "11211", "11222");
            AddNeighborhood("Sunset Park", "Bklyn", "Brooklyn", "11220", "11232");
            AddNeighborhood("Williamsburg - Bushwick", "Bklyn", "Brooklyn", "11206", "11221", "11237");
            AddNeighborhood("Central Harlem - Morningside Heights", "Mhtn", "Manhattan", "10026", "10027", "10030", "10037", "10039");
            AddNeighborhood("Chelsea - Clinton", "Mhtn", "Manhattan", "10001", "10011", "10018", "10019", "10020", "10036");
            AddNeighborhood("East Harlem", "Mhtn", "Manhattan", "10029", "10035");
            AddNeighborhood("Gramercy Park - Murray Hill", "Mhtn", "Manhattan", "10010", "10016", "10017", "10022");
            AddNeighborhood("Greenwich Village - Soho", "Mhtn", "Manhattan", "10012", "10013", "10014");
            AddNeighborhood("Lower Manhattan", "Mhtn", "Manhattan", "10004", "10005", "10006", "10007", "10038", "10280");
            AddNeighborhood("Union Square - Lower East Side", "Mhtn", "Manhattan", "10002", "10003", "10009");
            AddNeighborhood("Upper East Side", "Mhtn", "Manhattan", "10021", "10028", "10044", "10128");
            AddNeighborhood("Upper West Side", "Mhtn", "Manhattan", "10023", "10024", "10025");
            AddNeighborhood("Washington Heights - Inwood", "Mhtn", "Manhattan", "10031", "10032", "10033", "10034", "10040");
            AddNeighborhood("Bayside - Little Neck", "Qns", "Queens", "11361", "11362", "11363", "11364");
            AddNeighborhood("Flushing - Clearview", "Qns", "Queens", "11354", "11355", "11356", "11357", "11358", "11359", "11360");
            AddNeighborhood("Fresh Meadows", "Qns", "Queens", "11365", "11366", "11367");
            AddNeighborhood("Jamaica", "Qns", "Queens", "11412", "11423", "11432", "11433", "11434", "11435", "11436");
            AddNeighborhood("Long Island City - Astoria", "Qns", "Queens", "11101", "11102", "11103", "11104", "11105", "11106");
            AddNeighborhood("Ridgewood - Forest Hills", "Qns", "Queens", "11374", "11375", "11379", "11385");
            AddNeighborhood("Rockaway", "Qns", "Queens", "11691", "11692", "11693", "11694", "11695", "11697");
            AddNeighborhood("Southeast Queens", "Qns", "Queens", "11004", "11005", "11411", "11413", "11422", "11426", "11427", "11428", "11429");
            AddNeighborhood("Southwest Queens", "Qns", "Queens", "11414", "11415", "11416", "11417", "11418", "11419", "11420", "11421");
            AddNeighborhood("West Queens", "Qns", "Queens", "11368", "11369", "11370", "11372", "11373", "11377", "11378");
            AddNeighborhood("Port Richmond", "StnIs", "Staten Island", "10302", "10303", "10310");
            AddNeighborhood("South Beach - Tottenville", "StnIs", "Staten Island", "10306", "10307", "10308", "10309", "10312");
            AddNeighborhood("Stapleton - St. George", "StnIs", "Staten Island", "10301", "10304", "10305");
            AddNeighborhood("Willowbrook", "StnIs", "Staten Island", "10314");
        }

        private void AddNeighborhood(string fullName, string shortName, string borough, params string[] zipCodes)
        {
            Neighborhood neighborhood = new Neighborhood(fullName, shortName, borough);
            foreach (string zip in zipCodes)
                _neighborhoods.Add(zip, neighborhood);
        }

        private Dictionary<string, Neighborhood> _neighborhoods = new Dictionary<string, Neighborhood>();

        /// <summary>
        /// Formats the name of a <see cref="Location"/> into the name for the current
        /// locale.
        /// </summary>
        /// <param name="location">The location to format the name for.</param>
        /// <param name="format">The method that should be used to format the location name.</param>
        /// <returns>The name of the location, formatted to the locale.</returns>
        public override string FormatLocationName(Location location, LocationNameFormat format)
        {
            if ( location == null )
                throw new ArgumentNullException( "location" );
        

            Neighborhood nbhd = null;
            if (!_neighborhoods.TryGetValue(location.Address.PostalCode, out nbhd))
                nbhd = new Neighborhood(location.Address.City, location.Address.City, location.Address.City);

            string nbhdString = (format == LocationNameFormat.Default) ? nbhd.Borough : nbhd.ShortName;

            switch(location.Type)
            {
                case LocationType.CrossStreets:
                    return string.Format("{0}, {1}", location.Name, nbhdString);

                case LocationType.StreetAddress:
                    return string.Format("{0}, {1}", location.Address.Address1, nbhdString);

                default:
                    return location.Name;
            }
        }

        #region SearchParse

        /// <summary>
        /// Parses textual input of a search into a <see cref="LocationSearchCriteria"/> using rules
        /// for the current locale.
        /// </summary>
        /// <param name="searchString">The search to conduct.</param>
        /// <returns>The search criteria defined by the search string.</returns>
        public override LocationSearchCriteria ParseSearchString(string searchString)
        {
            return ParseSearch(searchString);
        }

        /// <summary>
        /// Parses textual input of a location to find into search criteria.
        /// </summary>
        /// <param name="searchString">The search to conduct.</param>
        /// <returns>The search criteria defined by the search string.</returns>
        public static LocationSearchCriteria ParseSearch(string searchString)
        {
            if (searchString == null)
                throw new ArgumentNullException("searchString");

            LocationSearchCriteria criteria = new LocationSearchCriteria(searchString, null);

            string query = ParseQuery(searchString, criteria);

            if (!ParseIntersection(query, criteria))
            {
                if (!ParseAddress(query, criteria))
                {
                    // Set a normal query if no other matches were returned.
                    criteria.SearchString = query;
                    criteria.Type = LocationType.Unknown;
                }
            }

            return criteria;
        }

        private static bool ParseIntersection(string query, LocationSearchCriteria criteria)
        {
            Match m = _intersectionRx.Match(query);

            if (!m.Success)
                return false;


            criteria.SearchString = string.Format("{0} & {1}", m.Groups["street1"], m.Groups["street2"]);
            criteria.Type = LocationType.CrossStreets;

            return true;
        }

        private static bool ParseAddress(string query, LocationSearchCriteria criteria)
        {
            Match m = _addressRx.Match(query);

            if (!m.Success)
                return false;


            criteria.SearchString = m.Groups["streetAll"].Value;
            criteria.Type = LocationType.StreetAddress;
            return true;
        }

        private static string ParseQuery(string searchString, LocationSearchCriteria criteria)
        {
            Match m = _queryRx.Match(searchString);
            if (!m.Success)
            {
                SetDefaultLocation(criteria);
                return searchString;
            }

            string query = m.Groups["query"].Value;

            bool localeSet = false;
            if (m.Groups["locale"].Success)
            {
                // TODO: Parse city and state out at some point
                criteria.City = m.Groups["locale"].Value;
                localeSet = true;
            }

            if (m.Groups["zip"].Success)
            {
                criteria.PostalCode = m.Groups["zip"].Value;
                localeSet = true;
            }

            if (!localeSet)
                SetDefaultLocation(criteria);

            return query;
        }

        private static void SetDefaultLocation(LocationSearchCriteria criteria)
        {
            criteria.City = "New York";
            criteria.State = "NY";
        }

        // In the future, parse query first, and then locale is found by the "near xxx" part or default.
        // then you can parse the rest using that locale.
        private static RegexOptions _rxoptions = RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Multiline;
        private static Regex _queryRx = new Regex(@"(?<query>(([^\s]|\s(?!(near\s|in\s))))+)((\s*,\s*|\s+near\s+|\s+in\s+)((?<locale>\D(\w|\s\D)+){0,1}[,;\s]*(?<zip>\d{5}){0,1})){0,1}", _rxoptions);
        private static Regex _addressRx = new Regex(@"(?<streetAll>(?<streetNumber>\d+)\s(?<street>.+))", _rxoptions);
        private static Regex _intersectionRx = new Regex(@"(?<streetAll>(?<street1>((\w|\s(?!(and\s|at\s|&\s))))+)(\s+and\s+|\s+at\s+|\s*&\s*)(?<street2>[\w\s]+))", _rxoptions);

        #endregion


        private class Neighborhood
        {
            public Neighborhood(string fullName, string shortName, string borough)
            {
                _fullName = fullName;
                _shortName = shortName;
                _borough = borough;
            }

            private string _fullName;

            public string FullName
            {
                get { return _fullName; }
            }

            private string _shortName;

            public string ShortName
            {
                get { return _shortName; }
            }

            private string _borough;

	        public string Borough
	        {
		        get { return _borough;}
	        }
	
        }
    }
}
